package com.flyme.common.security.filter;

import com.flyme.common.security.util.AntPathMatcher;
import com.flyme.common.security.util.WebUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Created by xinxingegeya on 2017/5/17.
 */
public abstract class AbstractAccessControlFilter extends NameableFilter implements PathConfigProcessor {

    private static final Logger log = LoggerFactory.getLogger(AbstractAccessControlFilter.class);
    /**
     * PatternMatcher used in determining which paths to react to for a given request.
     */
    protected AntPathMatcher pathMatcher = new AntPathMatcher();

    /**
     * A collection of path-to-config entries where the key is a path which this filter should process and
     * the value is the (possibly null) configuration element specific to this Filter for that specific path.
     */
    protected Map<String, Object> appliedPaths = new LinkedHashMap<String, Object>();

    /**
     * @param path   the application context path to match for executing this filter.
     * @param config the specified for <em>this particular filter only</em> for the given <code>path</code>
     * @return this configured filter.
     */
    @Override
    public Filter processPathConfig(String path, String config) {
        String[] values = null;
        if (config != null) {
            values = StringUtils.split(config, ",");
        }

        this.appliedPaths.put(path, values);
        return this;
    }

    /**
     * Returns <code>true</code> if the incoming <code>request</code> matches the specified <code>path</code> pattern,
     * <code>false</code> otherwise.
     *
     * @param path    the configured url pattern to check the incoming request against.
     * @param request the incoming HttpServletRequest
     * @return <code>true</code> if the incoming <code>request</code> matches the specified <code>path</code> pattern,
     * <code>false</code> otherwise.
     */
    protected boolean pathsMatch(String path, HttpServletRequest request) {
        String requestURI = getPathWithinApplication(request);
        log.trace("Attempting to match pattern '{}' with current requestURI '{}'...", path, requestURI);
        return pathsMatch(path, requestURI);
    }

    /**
     * Returns <code>true</code> if the <code>path</code> matches the specified <code>pattern</code> string,
     * <code>false</code> otherwise.
     *
     * @param pattern the pattern to match against
     * @param path    the value to match with the specified <code>pattern</code>
     * @return <code>true</code> if the <code>path</code> matches the specified <code>pattern</code> string,
     * <code>false</code> otherwise.
     */
    protected boolean pathsMatch(String pattern, String path) {
        return pathMatcher.matches(pattern, path);
    }

    /**
     * Returns the context path within the application based on the specified <code>request</code>.
     *
     * @param request the incoming <code>HttpServletRequest</code>
     * @return the context path within the application.
     */
    protected String getPathWithinApplication(HttpServletRequest request) {
        return WebUtils.getPathWithinApplication(request);
    }


    /**
     * Returns <code>true</code> if the request is allowed to proceed through the filter normally, or <code>false</code>
     * if the request should be handled by the
     * {@link #onAccessDenied(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) onAccessDenied(request,response)}
     * method instead.
     *
     * @param request  the incoming <code>HttpServletRequest</code>
     * @param response the outgoing <code>HttpServletResponse</code>
     * @return <code>true</code> if the request should proceed through the filter normally, <code>false</code> if the
     * request should be processed by this filter's
     * @throws Exception if an error occurs during processing.
     */
    protected abstract boolean isAccessAllowed(HttpServletRequest request, HttpServletResponse response) throws Exception;

    /**
     * Processes requests where the subject was denied access as determined by the
     * {@link #isAccessAllowed(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) isAccessAllowed}
     * method.
     *
     * @param request  the incoming <code>HttpServletRequest</code>
     * @param response the outgoing <code>HttpServletResponse</code>
     * @return <code>true</code> if the request should continue to be processed; false if the subclass will
     * handle/render the response directly.
     * @throws Exception if there is an error processing the request.
     */
    protected abstract boolean onAccessDenied(HttpServletRequest request, HttpServletResponse response) throws Exception;

    /**
     * Actually implements the chain execution logic
     *
     * @param request
     * @param response
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Exception exception = null;

        try {
            boolean continueChain = isFilterChainContinued(request, response);
            if (continueChain) {
                executeChain(request, response, filterChain);
            }
        } catch (Exception e) {
            exception = e;
        } finally {
            cleanup(request, response, exception);
        }
    }

    /**
     * Actually executes the specified filter chain by calling <code>chain.doFilter(request,response);</code>.
     * <p/>
     * Can be overridden by subclasses for custom logic.
     *
     * @param request  the incoming HttpServletRequest
     * @param response the outgoing HttpServletResponse
     * @param chain    the filter chain to execute
     * @throws Exception if there is any error executing the chain.
     */
    protected void executeChain(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws Exception {
        chain.doFilter(request, response);
    }

    /**
     * wrap the exception
     *
     * @param request  the incoming {@code HttpServletRequest}
     * @param response the outgoing {@code HttpServletResponse}
     * @param existing any exception that might have occurred while executing the {@code FilterChain} or
     *                 pre or post advice, or {@code null} if the pre/chain/post execution did not throw an {@code Exception}.
     * @throws ServletException if any exception other than an {@code IOException} is thrown.
     * @throws IOException      if the pre/chain/post execution throw an {@code IOException}
     */
    protected void cleanup(HttpServletRequest request, HttpServletResponse response, Exception existing)
            throws ServletException, IOException {
        if (existing != null) {
            if (existing instanceof ServletException) {
                throw (ServletException) existing;
            } else if (existing instanceof IOException) {
                throw (IOException) existing;
            } else {
                throw new ServletException(existing);
            }
        }
    }

    /**
     * Simple method to abstract out logic from the preHandle implementation - it was getting a bit unruly.
     */
    private boolean isFilterChainContinued(HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
            return true;
        }

        for (String path : this.appliedPaths.keySet()) {
            // If the path does match, then pass on to the subclass implementation for specific checks
            //(first match 'wins'):
            if (pathsMatch(path, request)) {
                return isAccessAllowed(request, response) || onAccessDenied(request, response);
            }
        }
        //This filter is disabled for this specific request,
        //return 'true' immediately to indicate that the filter will not process the request
        //and let the request/response to continue through the filter chain:
        return true;
    }


}
